#include "render_system.h"
#include "renderer/subrenderer/sprite_renderer.h"
#include "renderer/subrenderer/mesh_renderer.h"
#include "renderer/subrenderer/grid_renderer.h"

namespace vkrender
{
    RenderSystem::RenderSystem(
        const std::shared_ptr<VulkanDeviceContext>& context,
        const std::shared_ptr<VulkanSwapChain>& swapchain)
    {
        this->context = context;
        this->swapchain = swapchain.get();
        createCommandBuffers();

        sprite_renderer = std::make_shared<SpriteRenderer>(context, swapchain);
        mesh_renderer = std::make_shared<MeshRenderer>(context, swapchain);
        grid_renderer = std::make_shared<GridRenderer>(context, swapchain);

        glm::vec3 pos = glm::vec3(0.0f, 0.0f, 5.0f);
        float yaw = -180.0f;
        float pitch = 0.0f;
        camera3d = std::make_shared<Camera3D>(pos, yaw, pitch);
        camera2d = std::make_shared<Camera2D>();

        std::string objpath = "../resources/models/viking_room.obj";
        mesh = std::make_shared<VulkanModel>(context, objpath);
        grid = std::make_shared<Grid>(context);
    }

    RenderSystem::~RenderSystem()
    {
        freeCommandBuffers();
    }

    VkCommandBuffer RenderSystem::beginScene()
    {
        VkCommandBuffer cmd = commandBuffers[imageIndex];

        VkCommandBufferBeginInfo beginInfo{};
        beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;

        if (vkBeginCommandBuffer(cmd, &beginInfo) != VK_SUCCESS)
        {
            throw std::runtime_error("failed to begin recording command buffer!");
        }

        VkRenderPassBeginInfo renderPassInfo{};
        renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
        renderPassInfo.renderPass = swapchain->renderpass;
        renderPassInfo.framebuffer = swapchain->getFramebuffer(imageIndex);

        renderPassInfo.renderArea.offset = { 0, 0 };
        renderPassInfo.renderArea.extent = swapchain->swapchainExtent;

        std::array<VkClearValue, 2> clearValues{};
        clearValues[0].color = { 0.0f, 0.0f, 0.0f, 1.0f };
        clearValues[1].depthStencil = { 1.0f, 0 };
        renderPassInfo.clearValueCount = static_cast<uint32_t>(clearValues.size());
        renderPassInfo.pClearValues = clearValues.data();

        vkCmdBeginRenderPass(cmd, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);

        VkViewport viewport{};
        viewport.x = 0.0f;
        viewport.y = 0.0f;
        viewport.width = static_cast<float>(swapchain->getSwapchainExtent().width);
        viewport.height = static_cast<float>(swapchain->getSwapchainExtent().height);
        viewport.minDepth = 0.0f;
        viewport.maxDepth = 1.0f;
        VkRect2D scissor{ {0, 0}, swapchain->getSwapchainExtent() };
        vkCmdSetViewport(cmd, 0, 1, &viewport);
        vkCmdSetScissor(cmd, 0, 1, &scissor);

        return cmd;
    }

    VkCommandBuffer RenderSystem::endScene()
    {
        VkCommandBuffer cmd = commandBuffers[imageIndex];

        vkCmdEndRenderPass(cmd);
        if (vkEndCommandBuffer(cmd) != VK_SUCCESS)
        {
            throw std::runtime_error("failed to record command buffer!");
        }
        return cmd;
    }

    void RenderSystem::adjustForSwapchain(const std::shared_ptr<VulkanSwapChain>& swapchain)
    {
        this->swapchain = swapchain.get();
        if (swapchain->imageCount() != commandBuffers.size())
        {
            freeCommandBuffers();
            createCommandBuffers();
        }
#if 0
        // only renderpass is recreated, no need to recreate pipelines.
        grid_renderer->adjustForSwapchain(swapchain);
        sprite_renderer->adjustForSwapchain(swapchain);
        mesh_renderer->adjustForSwapchain(swapchain);
#endif
    }

    void RenderSystem::createCommandBuffers()
    {
        uint32_t num_frames = this->swapchain->imageCount();
        this->context->createCommandBuffers(num_frames, commandBuffers);
    }

    void RenderSystem::freeCommandBuffers()
    {
        if (commandBuffers.size() == 0)
            return;

        vkFreeCommandBuffers(
            context->device,
            context->commandPool,
            static_cast<uint32_t>(commandBuffers.size()),
            commandBuffers.data());
        commandBuffers.clear();
    }

    bool RenderSystem::beginFrame()
    {
        auto result = swapchain->acquireNextImage(&imageIndex);
        if (result == VK_ERROR_OUT_OF_DATE_KHR)
        { 
            return false;
        }

        if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR)
        {
            throw std::runtime_error("failed to acquire swap chain image!");
        }
        return true;
    }

    bool RenderSystem::endFrame()
    {
        VkCommandBuffer cmd = commandBuffers[imageIndex];
        VkResult result = swapchain->submitCommandBuffers(context->graphicsQueue, context->presentQueue, &cmd, &imageIndex);
        if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR)
        {
            return false;
        }
        else if (result != VK_SUCCESS)
        {
            throw std::runtime_error("failed to present swap chain image!");
        }
        return true;
    }

    void RenderSystem::DrawScene(VkExtent2D extent)
    {
        glm::mat4 proj3d = camera3d->getProjectionMatrix(extent.width, extent.height);
        glm::mat4 view3d = camera3d->getViewMatrix();

        VkCommandBuffer cmd = beginScene();

        // draw grids
        grid_renderer->bind(imageIndex, cmd, proj3d, view3d);
        grid_renderer->draw(imageIndex, cmd, grid);
        // draw mesh
        mesh_renderer->bind(imageIndex, cmd, proj3d, view3d);
        glm::mat4 model_matrix = glm::mat4(1.0f);
        model_matrix = glm::rotate(model_matrix, 45.0f, glm::vec3(1.0f, 1.0f, 0.0f));
        mesh_renderer->draw(imageIndex, cmd, mesh, model_matrix);

        // draw 2d sprites
        glm::mat4 proj2d = camera2d->getProjectionMatrix();
        glm::mat4 view2d = camera2d->getViewMatrix();
        sprite_renderer->bind(imageIndex, cmd, proj2d, view2d);
        glm::vec2 pos = { 0.0, 0.0 };
        glm::vec2 size = { 0.3, 0.4 };
        glm::vec3 color = { 0.3, 0.2, 0.5 };
        SpriteQuad sprite = { pos, size, color };

        sprite_renderer->draw(imageIndex, cmd, sprite);
        sprite.pos.x += 0.5;
        sprite_renderer->draw(imageIndex, cmd, sprite);
        sprite_renderer->flush(cmd);

        endScene();
    }
}
